fix(mode): fail loud on a half-configured client instead of silently reading the on-box store - #60
Conversation
…ilent empty read 0.2.91 honoured HASNA_KNOWLEDGE_API_URL. 0.2.92 does not: PR #43 made mode selection explicit-only, so a URL pointer with no HASNA_KNOWLEDGE_STORAGE_MODE resolves to the on-box store. That was deliberate and correct — a pointer exported in a login shell must never route writes at the live store. What it left behind is the mirror-image failure. `knowledge list --json` on a machine explicitly pointed at the hosted store answers `{"ok": true, "total": 0, "items": []}` with exit 0. Measured on station01: 98 entries locally against 869 in the store the URL names. Only `knowledge mode` ever mentioned the ignored pointer, and nothing runs `knowledge mode` before trusting a list. These tests fail on current HEAD by design; the fix follows. They assert the guard fires only on a genuinely ambiguous environment (a URL pointer with no mode var), that both remedies are named, that no pointer VALUE reaches stderr, and — the part that is easy to lose — that `knowledge mode` still answers in exactly the environment the guard rejects. Agent: vespasian
…the on-box store
Keeps 0.2.92's strictness — a pointer still never selects the backend — and
removes the silence it left behind. An API URL with no mode variable is an
ambiguous environment, and the client was resolving it to local and answering
`{"ok": true, "total": 0, "items": []}` with exit 0.
`assertKnowledgeModeSelected` is a gate on top of the resolution, not a change
to it. `resolveKnowledgeModeSelection` stays total and non-throwing because
`knowledge mode` resolves through it and has to keep answering in exactly the
environment the gate rejects — a guard fused into the resolver would take the
diagnostic down with the defect.
Scope is deliberately narrow so the check does not cry wolf:
- an API URL provokes it; an API key alone does not, because a key with no
URL points at no store and there is nothing to be ambiguous about
- `MODE=cloud`, `MODE=local` and an explicit `--store` all pass — each one is
the operator saying which store they meant
- a clean local install, which is nearly every invocation, is untouched
Applied at the CLI and at the MCP item-store resolver. The MCP is the surface
that needed it most: an agent calling an item tool never sees a `knowledge mode`
line, so it reads the empty result as an empty corpus.
Agent: vespasian
…-configured guard The committed generated artifacts were last rebuilt at d141994 and did not contain this PR's own new symbols, so the required CI check `Verify generated artifacts` failed deterministically. This was not merely a red light. package.json maps bin.knowledge-mcp to bin/knowledge-mcp.js, so the MCP half of the fix did not exist in the artifact that actually executes — a half-configured client reached through the MCP would still have silently fallen back to the on-box store. Rebuilt with `bun run build` under bun 1.3.14, matching the bun-version pinned in .github/workflows/ci.yml. Positive control on the rebuilt bundles: bin/knowledge.js HalfConfiguredKnowledgeClientError x1 (assertKnowledgeModeSelected is name-mangled by --minify) bin/knowledge-mcp.js HalfConfiguredKnowledgeClientError x3, assertKnowledgeModeSelected x2 bin/knowledge-serve.js and dist/*.js are byte-identical to what was already committed, which is correct: only src/cli.ts and src/mcp.js reach the new symbols. dist/knowledge-mode.d.ts is the tsc typing emit for the new module. Agent: vespasian
Blocker cleared — generated artifacts regeneratedPushed Root cause of the drift, and a trap worth recordingThe committed bundles were last rebuilt at The first rebuild attempt reproduced the symlinked- Committing that would have leaked a local filesystem path into the published bundle and left CI red, while spuriously touching four extra files. Fixed by replacing the symlink with a real Positive control — the symbols are actually in the bundlesGrepped after the clean rebuild, not inferred from a zero exit code:
The zeros are correct rather than a build miss: only The resulting diff is exactly three files — Local gate at the pinned bun with real ScopeOnly the regenerated artifacts. The five P2/P3 review findings are untouched and remain filed as separate follow-ups. Agent: vespasian |
CI green on
|
Finding: 0.2.92's local default was DELIBERATE HARDENING, not an accidental regression
Read the diff before choosing a fix. The behaviour change came in with PR #43
fix/f22a263f-explicit-mode-and-egress-guard(merge4226c25, 2026-07-28),which sits squarely inside the published 0.2.91 -> 0.2.92 window:
Nothing about it is accidental. It added
src/knowledge-mode.ts(191 lines,entirely about this one decision),
tests/knowledge-mode.test.ts(215 lines) withtests named "REGRESSION: url + key with no mode var stays LOCAL", a new
knowledge modecommand, and README changes. The module header states the ruleand the incident it came from: an ambient
HASNA_KNOWLEDGE_API_URL+HASNA_KNOWLEDGE_API_KEYexported in a login shell and inherited by every tmuxpane silently routed a test suite's writes at the live store, surfacing as a
flood of unrelated failures. It also closed a second inference layer in
@hasna/contractsviapinnedTransportEnv.So restoring 0.2.91's presence-based routing would reintroduce a known,
documented data-integrity incident. This PR does not do that.
What this PR changes
It keeps the strictness and removes the silence it left behind.
Measured on this branch before the fix, in a sandbox HOME with the pointer set
and no mode var:
A successful-looking empty page on a machine explicitly pointed at a store
holding 869 entries (station01 measured 98 local vs 869 hosted).
knowledge modereported the ignored pointer correctly the whole time — but nothing runs
knowledge modebefore trusting a list.Both silent readings are wrong for the same reason: the environment is
ambiguous and the client picked an answer instead of saying so.
assertKnowledgeModeSelectedis a gate on top of the resolution, not a changeto it.
resolveKnowledgeModeSelectionstays total and non-throwing, becauseknowledge moderesolves through it and must keep answering in exactly theenvironment the gate rejects — a guard fused into the resolver would take the
diagnostic down with the defect.
Scope is deliberately narrow so the check does not cry wolf:
points at no store, so there is nothing to be ambiguous about
MODE=cloud,MODE=local, and an explicit--storeall pass — each is theoperator saying which store they meant
Applied at the CLI and at the MCP item-store resolver. The MCP needed it most: an
agent calling an item tool never sees a
knowledge modeline, so it reads theempty result as an empty corpus.
Regression test first
bf1e360(tests) is committed beforeabfb24c(fix). Verified red againstthe test-only commit in a detached worktree, with the fix absent:
The one failing assertion is exactly the defect: exit 0 where it must be
non-zero. The 5 that pass on the old code are the guard rails —
knowledge modestill answering,
MODE=localstill listing,--storestill listing, and a cleanenv untouched — so the new suite is not simply "everything errors now".
Reviewer notes
HASNA_KNOWLEDGE_API_URLset and no mode var,every store-touching command now errors instead of returning local data. That
is the intent, and the remedy is one variable, named in the message. Worth a
deliberate look given the fleet-wide install.
deployment modes and says the OSS client picks its store by which connection
is configured, with "both configured" a hard boot error.
HASNA_KNOWLEDGE_STORAGE_MODEis therefore on death row, and this PR's error message tells operators to set it.
I still think this is right to land now — it makes today's shipped contract
honest rather than silent, and the "ambiguous config is a hard error" concept is
exactly what survives the mode var's removal; only the remedy text changes. But
it is a real tension between two owner-level decisions and the reviewer should
see it rather than discover it. There is already a
task/85b4eaca-remove-deployment-modesbranch in this repo.I was heavily throttled. Targeted red evidence above; CI is the green gate.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.